//
//  IOMonad+Cocoa.swift
//  MacDemo
//
//  Created by FH on 2020/12/3.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

// record has loads protocol types to ObjcInvokeProxy
private struct ImportProtocolTypes {
    static var loadTypes = Set<String>()
}

class CocoaDeallocObserver : Deinitable {
    let dealloc = DeallocObserver()
}

// REMARK: NSObject rac extension

extension NSObject {
    var rac_willDealloc: IOMonad<CocoaDeallocObserver> {
        var cmd = #function
        if let observer = objc_getAssociatedObject(self, &cmd) as? CocoaDeallocObserver {
            return observer.rac_willDealloc
        } else {
            let observer = CocoaDeallocObserver()
            objc_setAssociatedObject(self, &cmd, observer, .OBJC_ASSOCIATION_RETAIN)
            return observer.rac_willDealloc
        }
    }
    
    func rac_respondDelegate<ProtocolType: AnyObject, ValueType>(
        _ mapFn: @escaping (ProtocolType, String, [String : Any]) -> ValueType?) -> IOMonad<ValueType> {
        
        // load delegate type to proxy class
        let engine = sharedCommonDelegateEngine()
        let proxyTypeStr = String(describing: type(of: engine.proxy))
        let protocolTypeStr = String(describing: ProtocolType.self)
        if !ImportProtocolTypes.loadTypes.contains(protocolTypeStr) {
            let _ = importClass(proxyTypeStr) as ProtocolType
            ImportProtocolTypes.loadTypes.insert(protocolTypeStr)
        }
        // add target to engine
        let protocolType = NSProtocolFromString(protocolTypeStr)
        let target = sharedCocoaCommonCallbacksTarget(selector: nil, protocolType: protocolType)
        if let delegate = self.value(forKey: "delegate") {
            if !(delegate is ObjCInvokeProxy) {
                assert(self.value(forKey: "delegate") == nil, "\(self) has setted delegate.")
            }
        } else {
            engine.addTarget(target: target, runInMainQueue: true)
            // set delegate
            self.setValue(engine.proxy, forKeyPath: "delegate")
        }

        return IOMonad<ValueType> { thunker in
            let callbackId = target.onCallback(delegateProtocolType: protocolTypeStr) { (selector, argValues) in
                if let delegate = engine.proxy as? ProtocolType, let value = mapFn(delegate, selector, argValues) {
                    thunker.sendNext(value)
                }
            }

            return {
                target.removeCallback(callbackId: callbackId)
            }
        }.take(until: self.rac_willDealloc)
    }
}

// REMARK: NotificationCenter rac extenion

extension NotificationCenter {
    func rac_addObserver<ObjectType: AnyObject>(notifyName: NSNotification.Name, object: ObjectType)
        -> IOMonad<Notification> {
        return self.rac_addObserver(notifyName: notifyName.rawValue, object: object)
    }
    
    func rac_addObserver<ObjectType: AnyObject>(notifyName: String, object: ObjectType) -> IOMonad<Notification> {
        var signal = IOMonad<Notification> {[weak object] thunker in
            let observer = sharedCommonNotification()
            let selector = #selector(CommonObserver.onNotifyObserve(notify:))
            NotificationCenter.default.addObserver(observer,
                                                   selector: selector,
                                                   name: notifyName.toNotificationName(),
                                                   object: object)
            let callbackId = observer.addObserver(groupName: notifyName, notifyCallbackFn: { notify in
                if notify.name.rawValue == notifyName {
                    if let anyObject = object as? NSObject,
                        let notifyObject = notify.object as? NSObject,
                        anyObject.isEqual(notifyObject) {
                        thunker.sendNext(notify)
                    } else {
                        thunker.sendNext(notify)
                    }
                }
            }, kvoCallbackFn: nil)
            return {
                if let obj = object {
                    NotificationCenter.default.removeObserver(sharedCommonNotification(),
                                                              name: notifyName.toNotificationName(),
                                                              object: obj)
                    sharedCommonNotification().removeObserver(groupName: notifyName, callbackId: callbackId, callbackType: .notify)
                }
            }
        }
        if let anyObject = object as? NSObject {
            signal = signal.take(until: anyObject.rac_willDealloc)
        }
        return signal
    }
}

// REMARK: like RACObserve(object, prop)
fileprivate var KeyPathMapping = [Int : String]()

@dynamicMemberLookup
class RACObserve<Subject> where Subject : NSObject {
    let subject: Subject

    init(_ subject: Subject) {
        self.subject = subject
    }

    subscript<Property: Genericable>(dynamicMember keyPath: KeyPath<Subject, Property>) -> IOMonad<Property> {
        get {
            let subject = self.subject
            let typeName = String(describing: type(of: self.subject))
            let hashValue = self.subject.hashValue
            let propName = findMatchedPropName(keyPath: keyPath)
            return IOMonad {[weak subject] thunker in
                var callbackId: UUID? = nil
                if let propName = propName {
                    let observer = sharedCommonKVO()
                    callbackId = observer.addObserver(groupName: typeName, notifyCallbackFn: nil, kvoCallbackFn: { (key, val) in
                        if key == propName {
                            thunker.sendNext(val as! Property)
                        }
                    })
                    if let subject = subject {
                        observer.registerKVO(subject: subject, keyPath: propName)
                    }
                } else {
                    thunker.sendError(SimpleError.stringException("not found observe keyPath of \(typeName)"))
                }
                return {
                    let observer = sharedCommonKVO()
                    observer.unregisterKVO(typeName: typeName, subjectHashValue: hashValue)
                    if let callbackId = callbackId {
                        observer.removeObserver(groupName: typeName, callbackId: callbackId, callbackType: .kvo)
                    }
                }
            }.take(until: self.subject.rac_willDealloc)
        }
    }
    
    private func findMatchedPropName<Property>(keyPath: KeyPath<Subject, Property>) -> String? {
        if let propName = KeyPathMapping[keyPath.hashValue] {
            return propName
        }
        var propName: String? = nil
        var matchedProperty: [String : Any?] = [:]
        var propsCount : UInt32 = 0
        var mirror = Mirror(reflecting: self.subject)
        while let cls = mirror.subjectType as? AnyClass, cls != NSObject.self && cls != NSResponder.self {
            if let properties = class_copyPropertyList(cls, &propsCount) {
                for i : UInt32 in 0..<propsCount {
                    let cPropKey = property_getName(properties[Int(i)])
                    if let key = NSString(cString: cPropKey, encoding: String.Encoding.utf8.rawValue) as String?,
                        let cProperties = property_getAttributes(properties[Int(i)]),
                        let propProperties = NSString(cString: cProperties, encoding: String.Encoding.utf8.rawValue),
                        !(propProperties.contains(",R") || propProperties == "T:") {
                        let selector = NSSelectorFromString(key)
                        if self.subject.responds(to: selector), let value = self.subject.value(forKey: key) as? Property {
                            matchedProperty[key] = value
                        }
                    }
                }
                free(properties)
            }
            if mirror.superclassMirror != nil {
                mirror = mirror.superclassMirror!
            }
        }
        if matchedProperty.count > 1 {
            for (key, value) in matchedProperty {
                // set test-value
                if let generic = value! as? Genericable {
                    generic.setDetectPropValue(instance: self.subject, keyPath: key)
                    // get test-value
                    let testValue = self.subject[keyPath: keyPath]
                    if let valueBool = value as? Bool {
                        if equalsAny(lhs: testValue as Any, rhs: !valueBool) {
                            propName = key
                        }
                    } else if let type = Property.self as? Genericable.Type,
                        equalsAny(lhs: testValue, rhs: type.detectPropValue()) {
                        propName = key
                    }
                    // reset origin value
                    self.subject.setValue(value, forKey: key)
                    if propName != nil {
                        break
                    }
                }
            }
        } else {
            propName = matchedProperty.keys.first
        }
        if let propName = propName {
            KeyPathMapping[keyPath.hashValue] = propName
            return propName
        } else {
            return nil
        }
    }
}

// REMARK: Handle Cocoa Events

class FLButton : NSButton {
    fileprivate var enableOnFocusNotify = false
}

protocol ControlEventTouch {
    func addTarget(target: ObjCInvokeProxy, action: Selector)
}

extension ControlEventTouch where Self: NSObject {
    var rac_eventTouch: IOMonad<Self> {
        let selectorStr = "rac_eventTouch_targetAction:"
        let target = sharedCocoaCommonCallbacksTarget(selector: selectorStr, protocolType: nil)
        let engine = sharedCommonDelegateEngine()
        engine.addTarget(target: target, runInMainQueue: true)
        self.addTarget(target: engine.proxy, action: NSSelectorFromString(selectorStr))
        
        return IOMonad<Self> {[weak self] thunker in
            let callbackId = target.onCallback(selector: selectorStr, { argValues in
                if let self = self {
                    if let sender = argValues["sender"], self.isEqual(sender) {
                        thunker.sendNext(self)
                        if let btn = sender as? FLButton, btn.enableOnFocusNotify, let window = btn.window {
                            NotificationCenter.default.post(name: FLNotify_OnFocus_Key, object: window, userInfo: ["sender": btn])
                        }
                    }
                }
            })
            return {
                target.removeCallback(callbackId: callbackId)
            }
        }.take(until: self.rac_willDealloc)
    }
}

#if os(OSX)

extension NSButton : ControlEventTouch {
    func addTarget(target: ObjCInvokeProxy, action: Selector) {
        self.target = target
        self.action = action
    }

    var rac_valueDidChanged: IOMonad<Bool> {
        return self.rac_eventTouch.map { btn in
            return btn.state == .on
        }
    }
    
    var rac_sendActionName : IOMonad<String> {
        return self.rac_eventTouch.map { btn in
            return btn.flTitle
        }
    }
    
    func startObservrOnFocusEvent() {
        if let btn = self as? FLButton {
            btn.enableOnFocusNotify = true
        }
    }
    
    func stopObservrOnFocusEvent() {
        if let btn = self as? FLButton {
            btn.enableOnFocusNotify = false
        }
    }
}

extension NSTextField {
    var rac_valueDidChanged: IOMonad<String> {
        return self.rac_respondDelegate {[weak self](delegate: NSTextFieldDelegate, selector, argValues) -> String? in
            if selector == "controlTextDidChange:",
                let value = argValues["obj"] as? Notification,
                let input = value.object as? NSTextField,
                let self = self,
                self.isEqual(input) {
                return input.stringValue.trimmingCharacters(in: .whitespaces)
            }
            return nil
        }
    }
}

#else
#endif
